package com.github.facadefs.sftp;


import com.github.facadefs.FileView;
import com.github.facadefs.Selector;
import com.github.facadefs.support.SessionBasedFileSystem;
import com.github.facadefs.util.Paths;
import com.github.facadefs.util.Throwables;
import com.google.common.collect.Lists;
import com.sshtools.net.SocketTransport;
import com.sshtools.publickey.InvalidPassphraseException;
import com.sshtools.publickey.SshPrivateKeyFile;
import com.sshtools.publickey.SshPrivateKeyFileFactory;
import com.sshtools.sftp.*;
import com.sshtools.ssh.*;
import com.sshtools.ssh.components.SshPrivateKey;
import com.sshtools.ssh2.Ssh2Client;
import com.sshtools.ssh2.Ssh2Session;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.StringTokenizer;
import java.util.Vector;

/**
 * 基于 com.sshtools:j2ssh-maverick 实现的 SFTP 文件系统.
 * <p/>
 *
 * @author vacoor
 */
@Slf4j
@Getter
@Setter
public class J2sshMaverickSftpFileSystem extends SessionBasedFileSystem<J2sshMaverickSftpFileSystem.SftpChannel> {

    static class SftpChannel extends SftpSubsystemChannel {
        private final SshClient ssh;

        SftpChannel(SshSession sftp) throws SshException {
            super(sftp);
            this.ssh = sftp.getClient();
        }
    }

    private String host;
    private int port = 22;
    private String username;
    private String password;
    private byte[] privateKey;
    private byte[] passphrase;

    private String chroot;

    /**
     * {@inheritDoc}
     */
    @Override
    protected SftpChannel connect() throws IOException {
        final SshClient ssh = connectToServer();
        try {
            final Ssh2Session session = (Ssh2Session) ssh.openSessionChannel();
            final SftpChannel sftp = new SftpChannel(session);

            /*-
             * Start the SFTP server
             */
            if (!session.startSubsystem("sftp")) {
                log.debug("The SFTP subsystem failed to start, attempting to execute provider " + ssh.getContext().getSFTPProvider());

                // We could not start the subsystem try to fallback to the
                // provider specified by the user
                if (!session.executeCommand(ssh.getContext().getSFTPProvider())) {
                    session.close();
                    throw new IOException("Failed to start SFTP subsystem or SFTP provider " + ssh.getContext().getSFTPProvider());
                }
            }
            sftp.initialize();
            return sftp;
        } catch (final Exception e) {
            disconnect(ssh);
            throw new IOException("The SFTP Subsystem could not be initialized");
        }
    }

    private SshClient connectToServer() throws IOException {
        try {
            final SshConnector connector = SshConnector.createInstance();
            final SocketTransport transport = new SocketTransport(host, port);
            final Ssh2Client ssh = connector.connect(transport, username);

            SshAuthentication sshAuth;
            if (null != privateKey && privateKey.length > 0) {
                final SshPrivateKeyFile pkf = SshPrivateKeyFileFactory.parse(this.privateKey);
                final String passphraseStr = null != passphrase ? new String(passphrase, StandardCharsets.UTF_8) : null;
                final SshPrivateKey privateKeyToUse = pkf.toKeyPair(passphraseStr).getPrivateKey();
                final PublicKeyAuthentication pkc = new PublicKeyAuthentication();
                pkc.setUsername(username);
                pkc.setPrivateKey(privateKeyToUse);
                sshAuth = pkc;
            } else {
                final PasswordAuthentication pac = new PasswordAuthentication();
                pac.setUsername(username);
                pac.setPassword(password);
                sshAuth = pac;
            }

            final int code = ssh.authenticate(sshAuth);
            if (SshAuthentication.COMPLETE != code) {
                throw new IOException(String.format("Login failed on server - %s, port - %s", host, port));
            }
            return ssh;
        } catch (final SshException ex) {
            throw new IOException(String.format("Server - %s refused connection on port - %s: %s", host, port, ex.getMessage()), ex);
        } catch (final InvalidPassphraseException ex) {
            throw new IOException(String.format("Server - %s private key passphrase is invalid: %s", host, ex.getMessage()), ex);
        }
    }

    @Override
    protected void disconnect(final SftpChannel sftp) {
        if (null == sftp) {
            return;
        }
        try {
            try {
                if (!sftp.isClosed()) {
                    sftp.close();
                }
            } finally {
                disconnect(sftp.ssh);
            }
        } catch (final IOException skip) {
            log.warn("Server - {}:{} disconnect error: {}", host, port, skip.getMessage());
        }
    }

    private void disconnect(final SshClient ssh) {
        if (null != ssh && ssh.isConnected()) {
            ssh.disconnect();
        }
    }

    @Override
    protected FileView stat(final SftpChannel sftp, final String path) throws IOException {
        final String absPath = toAbsolutePath(path);
        try {
            final SftpFileAttributes stat = stat0(sftp, absPath);
            return null != stat ? toView(sftp, absPath, null, stat) : null;
        } catch (final Exception e) {
            return Throwables.rethrowIOException(e);
        }
    }

    private SftpFileAttributes stat0(final SftpChannel sftp, final String absPath) throws SftpStatusException, SshException {
        try {
            return sftp.getAttributes(absPath);
        } catch (final SftpStatusException e) {
            if (SftpStatusException.SSH_FX_NO_SUCH_FILE == e.getStatus() || SftpStatusException.SSH_FX_NO_SUCH_PATH == e.getStatus()) {
                return null;
            }
            throw e;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected FileView[] ls(final SftpChannel sftp, final String path) throws IOException {
        final String absPath = toAbsolutePath(path);
        try {
            final SftpFileAttributes stat = stat0(sftp, absPath);
            if (null == stat) {
                return new FileView[0];
            }
            if (!stat.isDirectory()) {
                return new FileView[]{toView(sftp, absPath, null, stat)};
            }

            final List<SftpFile> children = listChildren0(sftp, absPath);
            final List<FileView> views = Lists.newArrayListWithExpectedSize(children.size());
            for (final SftpFile child : children) {
                try {
                    final String filename = child.getFilename();
                    if (Paths.isCurrentDirName(filename) || Paths.isParentDirName(filename)) {
                        continue;
                    }
                    views.add(toView(sftp, child.getAbsolutePath(), child.getLongname(), child.getAttributes()));
                } finally {
                    child.close();
                }
            }
            return views.toArray(new FileView[0]);
        } catch (final Exception ex) {
            return Throwables.rethrowIOException(ex);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void ls(final SftpChannel sftp, final String path, final Selector selector) throws IOException {
        final String absPath = toAbsolutePath(path);
        try {
            final SftpFileAttributes stat = stat0(sftp, absPath);
            if (null == stat) {
                return;
            }
            if (!stat.isDirectory()) {
                selector.select(toView(sftp, absPath, null, stat));
                return;
            }

            final List<SftpFile> children = listChildren0(sftp, absPath);
            boolean skip = false;
            for (final SftpFile child : children) {
                try {
                    final String filename = child.getFilename();
                    if (Paths.isCurrentDirName(filename) || Paths.isParentDirName(filename)) {
                        continue;
                    }

                    final FileView v = toView(sftp, child.getAbsolutePath(), child.getLongname(), child.getAttributes());
                    skip = skip || Selector.CONTINUE != selector.select(v);
                } finally {
                    child.close();
                }
            }
        } catch (final Exception ex) {
            Throwables.rethrowIOException(ex);
        }
    }

    private List<SftpFile> listChildren0(final SftpChannel sftp, final String absPath) throws SftpStatusException, SshException {
        final SftpFile directory = sftp.openDirectory(absPath);
        final Vector<SftpFile> children = new Vector<SftpFile>();
        sftp.listChildren(directory, children);
        directory.close();
        return children;
    }

    @Override
    protected boolean mkdirs(final SftpChannel sftp, final String path) throws IOException {
        final String absPath = toAbsolutePath(path);
        try {
            /*-
             * FIXED BUG
             * <pre>
             * sftp.recurseMakeDirectory(absPath);
             * </pre>
             */
            if (canOpenDirectory(sftp, absPath)) {
                return true;
            }

            final StringTokenizer tokenizer = new StringTokenizer(absPath, Paths.PATH_SEPARATOR, true);
            String pathToUse = "";
            while (tokenizer.hasMoreElements()) {
                pathToUse = pathToUse + tokenizer.nextElement();

                if (!canOpenDirectory(sftp, pathToUse)) {
                    try {
                        sftp.makeDirectory(pathToUse);
                    } catch (final SftpStatusException e1) {
                        return false;
                    }
                }
            }
            return true;
        } catch (final Exception e) {
            return Throwables.rethrowIOException(e);
        }
    }

    private boolean canOpenDirectory(final SftpChannel sftp, final String absPath) throws SftpStatusException, SshException {
        try {
            sftp.openDirectory(absPath).close();
            return true;
        } catch (final SftpStatusException e) {
            if (SftpStatusException.SSH_FX_NO_SUCH_FILE == e.getStatus() || SftpStatusException.SSH_FX_NO_SUCH_PATH == e.getStatus()) {
                return false;
            }
            throw e;
        }
    }

    @Override
    protected void rm(final SftpChannel sftp, final String path, final boolean recurse) throws IOException {
        final String absPath = toAbsolutePath(path);
        try {
            final SftpFileAttributes stat = stat0(sftp, absPath);
            if (null == stat) {
                return;
            }
            if (stat.isDirectory()) {
                final List<SftpFile> children = listChildren0(sftp, absPath);
                if (!recurse && !children.isEmpty()) {
                    throw new IOException("You cannot delete non-empty directory, use recurse=true to override");
                }
                for (final SftpFile child : children) {
                    final String filename = child.getFilename();
                    if (Paths.isCurrentDirName(filename) || Paths.isParentDirName(filename)) {
                        child.close();
                        continue;
                    }
                    /*-
                     * child.getAbsolutePath is canonical path.
                     */
                    rm(sftp, toAbsolutePath(absPath, filename), recurse);
                    child.close();
                }
                sftp.removeDirectory(absPath);
            } else {
                sftp.removeFile(absPath);
            }
        } catch (final Exception ex) {
            Throwables.rethrowIOException(ex);
        }
    }

    @Override
    protected void rename(final SftpChannel sftp, final String oldPath, final String newPath) throws IOException {
        final String absOldPath = toAbsolutePath(oldPath);
        final String absNewPath = toAbsolutePath(newPath);
        try {
            // FIXME 可能是目录重命名
            sftp.renameFile(absOldPath, absNewPath);
        } catch (final Exception ex) {
            Throwables.rethrowIOException(ex);
        }
    }

    @Override
    protected InputStream open(final SftpChannel sftp, final String path) throws IOException {
        final String absPath = toAbsolutePath(path);
        try {
            final SftpFileAttributes stat = stat0(sftp, absPath);
            if (null == stat) {
                return null;
            }
            if (stat.isDirectory()) {
                throw new IOException("Can't open " + path + " because it is a directory");
            }
            return new SftpFileInputStream(sftp.openFile(absPath, SftpSubsystemChannel.OPEN_READ));
        } catch (final Exception ex) {
            return Throwables.rethrowIOException(ex);
        }
    }

    @Override
    protected OutputStream write(final SftpChannel sftp, final String path, final String contentType, final boolean override) throws IOException {
        final String absPath = toAbsolutePath(path);
        try {
            final SftpFileAttributes stat = stat0(sftp, absPath);
            if (null != stat) {
                if (!override) {
                    throw new IOException("File already exists: " + absPath);
                } else if (stat.isDirectory()) {
                    throw new IOException("Can't override " + path + " because it is a directory");
                }
            } else {
                final int i = absPath.lastIndexOf(Paths.PATH_SEPARATOR);
                final String absParentPath = i <= 0 ? null : absPath.substring(0, i);
                if (null != absParentPath && !this.mkdirs(sftp, absParentPath)) {
                    throw new IOException("write(): Mkdirs failed to create: " + absParentPath);
                }
            }
            return new SftpFileOutputStream(sftp.openFile(
                    absPath, SftpSubsystemChannel.OPEN_CREATE | SftpSubsystemChannel.OPEN_WRITE | SftpSubsystemChannel.OPEN_TRUNCATE
            ));
        } catch (final Exception ex) {
            return Throwables.rethrowIOException(ex);
        }
    }

    private String toAbsolutePath(final String path) {
        return toAbsolutePath(chroot, path);
    }

    private String toAbsolutePath(final String base, final String path) {
        return Paths.toAbsolutePath(base, path);
    }

    private String toRelativePath(final String path) {
        return Paths.toRelativePath(chroot, path);
    }

    private FileView toView(SftpSubsystemChannel sftp, String path, String longname, SftpFileAttributes attrs) throws IOException {
        long length = attrs.getSize().longValue();
        boolean dir = attrs.isDirectory();
        long lastModifiedTime = attrs.getModifiedTime().longValue() * 1000; // 毫秒
        long lastAccessTime = attrs.getAccessedTime().longValue() * 1000;
        int permissions = attrs.getPermissions().intValue();
        String uid = attrs.getUID();
        String gid = attrs.getGID();
        boolean isLink = attrs.isLink();
        String symlink = null;
        if (isLink) {
            try {
                symlink = sftp.getSymbolicLinkTarget(path);
            } catch (Exception ex) {
                symlink = null;
            }
        }
        String[] info = Utils.parseUserAndGroup(longname);
        String owner = 1 < info.length ? info[0] : String.valueOf(uid);
        String group = 1 < info.length ? info[1] : String.valueOf(gid);

        return new FileView(toRelativePath(path), length, dir, lastModifiedTime, lastAccessTime, permissions, owner, group, symlink);
    }
}
